Skip to main content

Add a Signing Key to an Existing Wallet ("Cold Boot")

This flow adds an initial BENJI intent-signing key to an existing BENJI wallet.

Use this flow when you see an INTENT_SIGNING_FAILURE, which refers to a successfully created walletId and Ed25519 public/private signing-key pair, but the wallet does not already have a signing key.

The flow has three main actions:

  1. Create the signing key with walletSigningKeyCreate
  2. Sign the returned intent payload locally with the Ed25519 private key
  3. Submit the signature with walletSigningKeySubmit

1. Understand the keys involved

There are three separate key concepts in this process:

  • BENJI authentication key — P-256 / ES256
    Used to authenticate the API client when requesting access to BENJI.
  • BENJI intent-signing key — Ed25519
    Used to sign BENJI intent payloads for wallet operations.
  • Blockchain wallet key — Network-specific
    Controls the underlying blockchain wallet address and is separate from BENJI authentication and intent signing.

For this flow, signingPublicKey and signingPrivateKey must be the matching Ed25519 key pair used for BENJI intent signing.

2. Required values

Before running the signing-key flow, configure the BENJI authentication, environment, wallet, and signing-key values used by the script.

Some values are provided by Franklin Templeton, some are generated during setup, and others are selected by the user.

Environment variableSourcePurposeExample / expected format
BENJI_CLIENT_IDProvided by Franklin TempletonBENJI OAuth client ID assigned during onboarding"0oa..."
BENJI_PRI_KEY_JSONGenerated during authentication setupOnboarded P-256 private JWK used to authenticate the API client{ "kty": "EC", "crv": "P-256", "x": "...", "y": "...", "d": "...", "kid": "..." }
BENJI_OKTA_URLStandard BENJI environment configurationBENJI OAuth token endpoint"https://login-preview.digitalassets.franklintempleton.com/oauth2/.../v1/token"
BENJI_SCOPESee Okta ScopesOAuth scope requested for the BENJI access tokenEach individual Okta scope, separated by a space. Example for read-only access: wallets.readonly bank_transactions.readonly transfers.readonly institutions.readonly
BENJI_ENDPOINTStandard BENJI environment configurationBENJI GraphQL API endpoint"https://api-uat.frk.com/tf/platform/bve/graphql"
WALLET_IDProvided by Franklin Templeton / BENJIBENJI-generated ID of the existing walletBENJI wallet ID, not the blockchain 0x... address
SIGNING_PUBLIC_KEYGenerated as part of the Ed25519 signing-key pairEd25519 public key registered with the walletBase64-encoded public key
SIGNING_PRIVATE_KEYGenerated as part of the Ed25519 signing-key pairMatching Ed25519 private key used to sign the returned intent payloadBase64-encoded private key
SIGNING_KEY_NAMESelected by the userName assigned to the new signing key"InitialKey"

Example local configuration

A configured PowerShell session should therefore resemble:

# Provided by Franklin Templeton
$env:BENJI_CLIENT_ID = "your-client-id"
$env:WALLET_ID = "your-benji-wallet-id"

# Generated during authentication setup
$env:BENJI_PRI_KEY_JSON = Get-Content -Raw "C:\path\to\benji-private-jwk.json"

# BENJI UAT environment
$env:BENJI_OKTA_URL = "https://login-preview.digitalassets.franklintempleton.com/oauth2/aus3nefxjv7YUkjW31d7/v1/token"
$env:BENJI_ENDPOINT = "https://api-uat.frk.com/tf/platform/bve/graphql"

# Generated Ed25519 signing-key pair
$env:SIGNING_PUBLIC_KEY = "your-ed25519-public-key"
$env:SIGNING_PRIVATE_KEY = "your-ed25519-private-key"

# Selected by the user
$env:SIGNING_KEY_NAME = "InitialKey"

3. Create the signing key

The first mutation registers the Ed25519 public key with the existing wallet.

/**
* Create a signing key on an existing wallet.
*
* The wallet must not already have a signing key. The mutation returns the
* signingKeyId and the payload that must be signed before the key is submitted.
*/
export async function walletSigningKeyCreate(
walletId,
signingKeyName,
signingKey
) {
const QUERY = gql`
mutation walletSigningKeyCreate(
$walletId: ID!
$signingKeyName: String!
$signingKey: String!
) {
walletSigningKeyCreate(
input: {
walletId: $walletId
signingKeyName: $signingKeyName
signingKey: $signingKey
}
) {
signingKeyId
intentSigningPackage {
payloadType
payload
}
}
}
`;

const variables = {
walletId,
signingKeyName,
signingKey
};

try {
return await client.request(QUERY, variables);
} catch (err) {
console.error('GraphQL error:', err);
throw err;
}
}

The important values returned are:

signingKeyId
intentSigningPackage.payload

The payload must be signed before the signing key can be submitted.

4. Sign the returned intent payload

Call walletSigningKeyCreate using the wallet ID, key name, and Ed25519 public key:

const signingKeyCreateResult = await walletSigningKeyCreate(
walletId,
signingKeyName,
signingPublicKey
);

Then retrieve the payload returned by BENJI and sign it with the matching Ed25519 private key:

const intentSignature = intentSignPayload(
signingPrivateKey,
signingKeyCreateResult.walletSigningKeyCreate
.intentSigningPackage.payload
);

The exact returned payload is what must be signed. Do not substitute or reconstruct the payload.

5. Submit the signed intent

The second mutation takes:

  • the signingKeyId returned by walletSigningKeyCreate
  • the generated intentSignature
/**
* Submit the signed intent returned by walletSigningKeyCreate.
*/
export async function walletSigningKeySubmit(
signingKeyId,
intentSignature
) {
const QUERY = gql`
mutation walletSigningKeySubmit(
$signingKeyId: ID!
$intentSignature: String!
) {
walletSigningKeySubmit(
institutionSubmit: {
signingKeyId: $signingKeyId
intentSignature: $intentSignature
}
) {
signingKeyId
blockchainStatus
}
}
`;

const variables = {
signingKeyId,
intentSignature
};

try {
return await client.request(QUERY, variables);
} catch (err) {
console.error('GraphQL error:', err);
throw err;
}
}

Submit the result:

const signingKeySubmitResult = await walletSigningKeySubmit(
signingKeyCreateResult.walletSigningKeyCreate.signingKeyId,
intentSignature
);

console.log(
'GraphQL response:\n',
JSON.stringify(signingKeySubmitResult, null, 2)
);

The response returns:

signingKeyId
blockchainStatus

6. Complete script flow

The core execution flow is therefore:

const signingPublicKey = process.env.SIGNING_PUBLIC_KEY;
const signingPrivateKey = process.env.SIGNING_PRIVATE_KEY;
const walletId = process.env.WALLET_ID;
const signingKeyName =
process.env.SIGNING_KEY_NAME || 'InitialKey';

if (!walletId || !signingPublicKey || !signingPrivateKey) {
throw new Error(
'Set WALLET_ID, SIGNING_PUBLIC_KEY, and SIGNING_PRIVATE_KEY.'
);
}

// Step 1: Create the signing key
const signingKeyCreateResult = await walletSigningKeyCreate(
walletId,
signingKeyName,
signingPublicKey
);

console.log(
'GraphQL response:\n',
JSON.stringify(signingKeyCreateResult, null, 2)
);

// Step 2: Sign the returned BENJI intent payload
const intentSignature = intentSignPayload(
signingPrivateKey,
signingKeyCreateResult.walletSigningKeyCreate
.intentSigningPackage.payload
);

// Step 3: Submit the signing key and signature
const signingKeySubmitResult = await walletSigningKeySubmit(
signingKeyCreateResult.walletSigningKeyCreate.signingKeyId,
intentSignature
);

console.log(
'GraphQL response:\n',
JSON.stringify(signingKeySubmitResult, null, 2)
);